﻿// DEPENDENCIES:
//   Javascript
//   - jquery
//   - jquery.validate
//   - jquery.form
//   - jquery.metadata

using System.Collections.Generic;
using System.Text;

namespace BigfootMVC.Helpers
{

    /// <summary>
    /// This class helps us to create javascript code using server side code for commonly performed 
    /// operation. Although I do not like inline javascript, there are some times in the interested
    /// of productivity when it helps to make these items accessbile and repeatable. The funcitons  
    /// in this helper are particularly useful when using in conjunction with the HtmlHelper and
    /// tag builder classes.
    /// 
    /// It abstracts repetive tasks when making ajax calls, validating input, and adding functionality
    /// not currently present in asp.net in an easily repeatable manner.
    /// 
    /// Like most of the helpers this helper has a fluid interface as well. You can compose 
    /// functionality by calling methods on it before rendering the script.
    /// </summary>
    public class JSBuilder
    {
        
        private StringBuilder _data = new StringBuilder();

        /// <summary>
        /// This is a static function to easily create a new JSHelper function inline
        /// </summary>
        public static JSBuilder Create()
        {
            return new JSBuilder();
        }

        /// <summary>
        /// Returns a properly formatted string to be include in Javascript, it also appends the quotes to the string
        /// this function also calls the CleanString to make sure any characters not allowed in javascript are safely scaped
        /// </summary>
        /// <param name="value">The string to clean without the beggining and ending literal quotes</param>
        public static string GetString(string value)
        {
            return string.Format("'{0}'", CleanString(value));
        }

        /// <summary>
        /// Escapes any characters not allowed in javascript
        /// </summary>
        public static string CleanString(string value)
        {
            if (string.IsNullOrEmpty(value)) return value;

            var sb = new StringBuilder();
            foreach (var c in value)
            {
                switch (c)
                {
                    case '\"':
                        sb.Append("\\\"");
                        break;
                    case '\'':
                        sb.Append("\\\'");
                        break;
                    case '\\':
                        sb.Append("\\\\");
                        break;
                    case '\b':
                        sb.Append("\\b");
                        break;
                    case '\f':
                        sb.Append("\\f");
                        break;
                    case '\n':
                        sb.Append("\\n");
                        break;
                    case '\r':
                        sb.Append("\\r");
                        break;
                    case '\t':
                        sb.Append("\\t");
                        break;
                    default:
                        var i = (int)c;
                        if (i < 32 || i > 127)
                        {
                            sb.AppendFormat("\\u{0:X04}", i);
                        }
                        else
                        {
                            sb.Append(c);
                        }
                        break;
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// Adds raw string to the JS builder. Be aware of line termination etc. As this does not do any of this for you
        /// it simple calls stringbuilder.append on the internal object. User other methods like AddLine to add a complete oneline statment to the script
        /// </summary>
        /// <param name="value">Javascript to add to the builder. i.e. jQuery("#test").addClass("newclass"); </param>
        public JSBuilder Add(string value)
        {
            _data.Append(value);
            return this;
        }

        /// <summary>
        /// Adds raw string to the JS builder using string.format. Be aware of line termination etc. As this does not do any of this for you
        /// it simple calls stringbuilder.append on the internal object. User other methods like AddLine to add a complete oneline statment to the script
        /// </summary>
        /// <param name="value">Javascript to add to the builder. i.e. jQuery("#test").addClass("{0}"); </param>
        /// <param name="params">List of values to use for the string.format</param>
        public JSBuilder Add(string value, params string[] @params)
        {
            _data.AppendFormat(value, @params);
            return this;
        }

        /// <summary>
        /// Adds a complete line of javascript including properly terminating the line with the semicollon
        /// </summary>
        /// <param name="line">Javascript line i.e. jQuery("test").addClass("one")</param>
        public JSBuilder AddLine(string line)
        {
            return Add(" " + line).EndLine();
        }

        /// <summary>
        /// Adds a complete line of javascript including properly terminating the line with the semicollon using string.format
        /// </summary>
        /// <param name="line">Javascript line i.e. jQuery("test").addClass("{0}")</param>
        /// <param name="params">The values to merge using string.format</param>
        public JSBuilder AddLine(string line, params string[] @params)
        {
            return Add(" " + line, @params).EndLine();
        }

        /// <summary>
        /// Adds a semicollon to the builder
        /// </summary>
        public JSBuilder EndLine()
        {
            if (!_data.ToString().Trim().EndsWith(";")) 
                Add("; ");
            return this;
        }

        /// <summary>
        /// Determines if the script builder is empty, meaning no commands have been added ot it.
        /// </summary>
        public bool IsEmpty()
        {
            return _data.Length == 0;
        }

        /// <summary>
        /// Wraps a validation check around the current script. It selects the form that is the parent of the current element
        /// </summary>
        public JSBuilder WrapParentFormValidation()
        {
            const string format = "if (jQuery(this).parents('form:eq(0)').validate({{ onsubmit: false, debug: false }}).form()) {{ {0} }}";
            return Wrap(format);
        }

        /// <summary>
        /// Wraps a validation check around the current script for the form specified
        /// </summary>
        public JSBuilder WrapFormValidation(string formName)
        {
            var format = "if (jQuery('" + JQHelper.GetElementID(formName) + "').validate({{ onsubmit: false, debug: false }}).form()) {{ {0} }}";
            return Wrap(format);
        }

        /// <summary>
        /// Wraps a validate check around the current script only for the input names specified
        /// </summary>
        public JSBuilder WrapInputsValidation(params string[] inputNames)
        {
            if (inputNames == null || inputNames.Length == 0) return this;

            var selector = "";
            foreach (var field in inputNames)
            {
                if (selector.Length > 0) selector += ", ";
                selector += JQHelper.GetElementID(field);
            }

            var wrapper = "var isValid = true; ";
            wrapper += "jQuery('" + selector + "').each(function(i, item) {{ if (!jQuery(item).valid()) isValid = false; }}); ";
            wrapper += "if (isValid) {{ {0} }}";

            Wrap(wrapper);
            return this;
        }

        /// <summary>
        /// Wraps a validate check around all the inputs within the specified contianer
        /// </summary>
        public JSBuilder WrapContainerValidation(string containerId)
		{
            // This is the pre part
            var preScript = new StringBuilder();
            preScript.Append(" var isValid = true; ");
            preScript.Append(JQ.SelectFields(JQHelper.GetElementID(containerId))
                               .each("if (!jQuery(item).valid()) isValid = false;").ToString(true));
            preScript.Append("if (isValid) { ");
            
            // Insert the pre into the beginning
            _data.Insert(0, preScript.ToString());

            // Close the wrap and return
            return AddLine("}");
		}

        /// <summary>
        /// Wraps a validation check around the current script for the group specified
        /// </summary>
        /// <param name="validationGroup">The group being validated</param>
        public JSBuilder WrapGroupValidation(string validationGroup)
        {
            if (validationGroup.StartsWith("#")) validationGroup = validationGroup.Substring(1);
            var format = "if (ValidateGroup_" + validationGroup + "()) {{ {0} }}";
            return Wrap(format);
        }
        
        /// <summary>
        /// Wraps a confirmation check around the current script
        /// </summary>
        /// <param name="message">The message to use in the confirmation</param>
        /// <returns></returns>
        public JSBuilder WrapConfirm(string message)
        {
            const string format = "if (confirm({1})) {{ {0} }}";
            return Wrap(format, GetString(message));
        }

        /// <summary>
        /// Wraps the current script with the code specified
        /// </summary>
        /// <param name="format">The format to use. {0} is reserved for the current contents of the script.  
        /// i.e. if(value == {1}) {{ {0} }}; Use double quotes to escape { and }</param>
        /// <param name="params">Values to merge with your script.</param>
        /// <returns></returns>
        public JSBuilder Wrap(string format, params string[] @params)
        {
            // Create the parameter object {0} is reserved for the current string
            var newparams = new List<string> {_data.ToString()};
            if (@params.Length != 0)
            {
                newparams.AddRange(@params);
            }

            // Format the value
            var newvalue = string.Format(format, newparams.ToArray());

            // Replace existing value
            _data = new StringBuilder(newvalue);
            return this;
        }

        /// <summary>
        /// Returns the result of the built script
        /// </summary>
        public override string ToString()
        {
            return _data.ToString();
        }

        /// <summary>
        /// Returns the result of the built script in a way that is suitable for inline script. 
        /// i.e. javascript:yourscript;
        /// </summary>
        public string ToInline()
        {
            var script = ToString().TrimEnd();
            if (!script.EndsWith(";")) script += "; ";
            return "javascript:" + script + "void(0);";
        }

        /// <summary>
        /// Shortcut function that creates a JQ object
        /// </summary>
        public JQHelper JQ { get { return JQHelper.Create();} }

    }


}